﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;

namespace ObjectMerger.Extensions
{
    /// <summary>
    /// Extension methods for merging objects.
    /// </summary>
    public static class MergeExtensions
    {
        /// <summary>
        /// Merges an object with another object.
        /// </summary>
        /// <typeparam name="T">The type of the object.</typeparam>
        /// <param name="source">Source object.</param>
        /// <param name="target">Target object.</param>
        /// <returns>The merged object, makes fluent interface possible: objectC = objectA.MergeWith(objectB).</returns>
        public static T MergeWith<T>(this T source, T target) where T : class
        {
            if (source == null) return target;
            if (target == null) return source;

            if (source.GetType() != target.GetType())
                throw new ArgumentException("Parameters must have the same type.");

            if (IsCollection(source.GetType()))
            {
                MergeCollection(source as IList, target as IList);
            }
            else
            {
                MergeObject(source, target);
            }

            return source;
        }

        private static void MergeObject<T>(T source, T target) where T : class
        {
            Type sourceTypeObject = source.GetType();
            Type targetTypeObject = target.GetType();

            PropertyInfo[] sourceProperties = sourceTypeObject.GetProperties();
            PropertyInfo[] targetProperties = targetTypeObject.GetProperties();

            for (int i = 0; i < sourceProperties.Length; i++)
            {
                object sourceValue = sourceProperties[i].GetValue(source, null);
                object targetValue = targetProperties[i].GetValue(target, null);

                // Determine if the type is a Collection
                if (IsCollection(sourceProperties[i].PropertyType))
                {
                    // Property is a collection, merge collection
                    MergeCollection(source as IList, target as IList);
                }
                // Determine if type must be overwritten
                else if (CanBeOverWritten(sourceProperties[i].PropertyType) &&
                        HasValue(targetValue))
                {
                    sourceProperties[i].SetValue(source, targetValue, null);
                }
                // Determine if the type if an object (class)
                else
                {
                    MergeWith(source, target);
                }
            }
        }

        private static void MergeCollection(IList source, IList target)
        {
            if (source.GetType() != target.GetType())
                throw new ArgumentException("Parameters must have the same type.");

            if (source is IComparable && target is IComparable)
            {
                // If possible, sort the lists.
                ArrayList.Adapter((IList)source).Sort();
                ArrayList.Adapter((IList)target).Sort();
            }

            // Check if items are added in the target list.
            if (source.Count < target.Count)
            {
                for (int i = source.Count; i < target.Count; i++)
                {
                    source.Add(target[i]);
                }
            }

            for (int i = 0; i < source.Count; i++)
            {
                if (HasValue(target[i].GetType()))
                {
                    // Target listitem has a valid value.
                    source[i] = target[i];
                }
                else
                {
                    // Merge listitems
                    source[i] = MergeWith(source[i], target[i]);
                }
            }
        }

        private static bool IsCollection(Type type)
        {
            return (type is ICollection) || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>));
        }

        private static bool CanBeOverWritten(Type type)
        {
            return ((type is Enum) || (type is String) || (type is ValueType) || (type is Object));
        }

        private static bool HasValue(object waarde)
        {
            if (waarde == null) return false;

            return !string.IsNullOrEmpty(waarde.ToString());
        }
    }
}
